import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
import { Helper } from '../tool/helper';
import { Quaternion, Vector2, Vector3 } from '@babylonjs/core/Maths/math.vector';
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
import { Color3 } from '@babylonjs/core/Maths/math.color';
import { PointerEventTypes, PointerInfo } from '@babylonjs/core/Events/pointerEvents';
import { AnimationFrame } from '../animation/animation-frame';
import { Scene } from '@babylonjs/core/scene';
import { SetPointerEvent } from '../misc/set-pointer-event';


/**
 * 观察层参数对象
 * @param maskRenderingGroupId 遮罩平面渲染顺序值
 * @param modelRenderingGroupId 模型渲染顺序值
 * @param maskSize 遮罩平面大小
 * @param maskColor 遮罩平面颜色
 * @param maskOpacity 遮罩平面不透明度
 * @param rotateDelay 旋转延迟（为避免相机粘滞效果的负面影响）
 * @param verticalRotateLimit 垂直旋转限制
 * @param speedRatio 旋转速度比率
 * @param maskPickable 遮罩是否可被拾取
 * @param hideMode 显隐模式
 * @param useRotate 启用旋转（通用：单触点右半屏滑动）
 * @param useTranslate 启用平移（通用：单触点左半屏滑动，移动端：双指）
 * @param useZoom 启用缩放（通用：上/下半屏幕按住不动，pc：滚轮，移动端：双触点）
)
 * @param minZoom 缩放下限
 * @param maxZoom 缩放上限
 * @param pressThreshold 缩放操作的按下时间
 * @param pressArea 缩放操作的按下区域大小
 * @param autoRotate 是否自动旋转
 * @param autoRotateDelay 自动旋转延迟
 * @param autoRotateDelta 自动旋转增量
 * @param wheelZoomRatio 滚轮缩放速度
 * @param touchZoomRatio 触摸缩放速度
 * @param touchMoveRatio 触摸平移速度
 */
interface IObserveLayerOptions {
    maskRenderingGroupId?: number,
    modelRenderingGroupId?: number,
    maskSize?: number,
    maskColor?: Color3,
    maskOpacity?: number,
    rotateDelay?: number,
    verticalRotateLimit?: number,
    speedRatio?: number,
    maskPickable?: boolean,
    hideMode?: 'visible' | 'scaling',
    useRotate?: boolean,
    useTranslate?: boolean,
    useZoom?: boolean,
    minZoom?: number,
    maxZoom?: number,
    pressThreshold?: number,
    pressArea?: number,
    autoRotate?: boolean,
    autoRotateDelay?: number,
    autoRotateDelta?: Quaternion,
    wheelZoomRatio?: number,
    touchZoomRatio?: number,
    touchMoveRatio?: number,
}


/**
 * 观察层（在场景上层，用于结构展示或其它功能）
 * 右半区域拖动旋转，左半区域拖动平移，上半区域按住放大，下半区域按住缩小；
 * pc额外支持滚轮缩放；
 * 移动端额外支持双指缩放和平移；
 * @param scene 所在场景
 * @param options 参数对象
 */
class ObserveLayer {

    private _container: TransformNode;
    private _mask!: AbstractMesh;
    private _modelContainerP!: TransformNode;
    private _modelContainerH!: TransformNode;
    private _modelContainerV!: TransformNode;
    private _modelContainer!: TransformNode;
    private _pointerHandle: { (e: PointerInfo): void };
    private _singleStart = false;
    private _preDir = new Vector3();
    private _req = {};
    private _tv0 = new Vector3();
    private _tq0 = new Quaternion();
    private _tq1 = new Quaternion();
    private _rc = 0;
    private _pointerPress = new Vector2();
    private _pointerMove = new Vector2();
    private _pressReq = {};
    private _isLeft = 2;
    private _isTop = 4;
    private _ops: IObserveLayerOptions = {};
    private _enabled = false;
    private _autoRotateReq = {};
    private _autoRotateDelayReq = {};

    private _zoomWheelReq = {};
    private _zoomPrg = 0;
    private _zoomSpd = 0;

    private _pointers: PointerEvent[] = [];
    private _multiStart = false;
    private _touchS = new Vector2();
    private _touchE = new Vector2();
    private _scaleRef = 0;
    private _transRef = new Vector2();
    private _transTmp = new Vector2();

    public readonly scene: Scene;

    constructor(scene: Scene, options?: IObserveLayerOptions) {
        const ops = options || {};
        this._ops.maskRenderingGroupId = ops.maskRenderingGroupId || 1;
        this._ops.modelRenderingGroupId = ops.modelRenderingGroupId || 2;
        this._ops.maskSize = ops.maskSize || 10;
        this._ops.maskColor = ops.maskColor || new Color3(0, 0, 0);
        this._ops.maskOpacity = ops.maskOpacity !== undefined ? ops.maskOpacity : 0.9;
        this._ops.rotateDelay = ops.rotateDelay || 200;
        this._ops.verticalRotateLimit = ops.verticalRotateLimit || 0.8;
        this._ops.speedRatio = ops.speedRatio || 2;
        this._ops.hideMode = ops.hideMode || 'visible';
        this._ops.useRotate = ops.useRotate === undefined ? true : ops.useRotate;
        this._ops.useTranslate = !!ops.useTranslate;
        this._ops.useZoom = !!ops.useZoom;
        this._ops.minZoom = ops.minZoom || 0.01;
        this._ops.maxZoom = ops.maxZoom || 100;
        this._ops.pressThreshold = ops.pressThreshold || 300;
        this._ops.pressArea = ops.pressArea || 400;
        this._ops.autoRotate = !!ops.autoRotate;
        this._ops.autoRotateDelay = ops.autoRotateDelay || 5000;
        this._ops.autoRotateDelta = ops.autoRotateDelta || Quaternion.FromEulerAngles(0, 0.01, 0);
        this._ops.wheelZoomRatio = ops.wheelZoomRatio || 0.0002;
        this._ops.touchZoomRatio = ops.touchZoomRatio || 1;
        this._ops.touchMoveRatio = ops.touchMoveRatio || 0.01;

        this._container = new TransformNode('observe-layer-container', scene);
        this.scene = scene;
        this.initializeModelContainer();
        this.initializeMask();
        this._mask.isPickable = ops.maskPickable || false;
        this.update();

        const LEFT = 10, RIGHT = 11, TOP = 100, BOTTOM = 110;
        const CAN_ROT = 1000, CANNOT_ROT = 1100, CAN_TRS = 10000, CANNOT_TRS = 11000;
        let useRotate = this._ops.useRotate ? CAN_ROT : CANNOT_ROT;
        let useTranslate = this._ops.useTranslate ? CAN_TRS : CANNOT_TRS;

        this._pointerHandle = (evt: PointerInfo) => {
            if (!this._enabled) return;
            const e = SetPointerEvent(evt, this.scene);
            switch (e.type) {
                case PointerEventTypes.POINTERDOWN:
                    if (this._ops.useZoom || this._ops.useTranslate) {
                        const pd = evt.event as PointerEvent;
                        if (this.hasPointer(pd) < 0) {
                            this._pointers.push(pd);
                        }
                        if (this._pointers.length == 2) {
                            this._multiStart = true;
                        } else if (this._pointers.length > 2) {
                            this._multiStart = false;
                            this._singleStart = false;
                            this._pointers.length = 0;
                            AnimationFrame.cancel(this._pressReq);
                            AnimationFrame.cancel(this._zoomWheelReq);
                            this.autoRotate(true);
                            return;
                        } else {
                            this._multiStart = false;
                        }
                    }
                    if (this._multiStart) {
                        this._touchS.set(this._pointers[0].clientX, this._pointers[0].clientY);
                        this._touchE.set(this._pointers[1].clientX, this._pointers[1].clientY);
                        this._scaleRef = Vector2.Distance(this._touchS, this._touchE);
                        this._transRef.copyFrom(this._touchS).addInPlace(this._touchE).scaleInPlace(0.5);

                        this._singleStart = false;
                        AnimationFrame.cancel(this._pressReq);
                        return;
                    }

                    if (this._singleStart) return;
                    if (!e.pickInfo) return;
                    if (!e.pickInfo.ray) return;
                    const engine = this.scene.getEngine();
                    const pr = engine.getHardwareScalingLevel();
                    const w = engine.getRenderWidth() * pr;
                    const h = engine.getRenderHeight() * pr;
                    const x = e.event.x;
                    const y = e.event.y;
                    this._isLeft = x < w * 0.5 ? LEFT : RIGHT;
                    this._isTop = y < h * 0.5 ? TOP : BOTTOM;
                    this._preDir.copyFrom(e.pickInfo.ray.direction);
                    this._pointerPress.set(x, y);
                    this._pointerMove.set(x, y);
                    if (this._ops.useZoom) this.checkPress(e, (y - h * 0.5) / (h * 0.5));
                    this._singleStart = true;
                    this.autoRotate(false);
                    break;
                case PointerEventTypes.POINTERMOVE:
                    if (this._multiStart) {
                        this.setPointer(evt.event as PointerEvent);
                        this._touchS.set(this._pointers[0].clientX, this._pointers[0].clientY);
                        this._touchE.set(this._pointers[1].clientX, this._pointers[1].clientY);
                        if (this._ops.useZoom) {
                            const length = Vector2.Distance(this._touchS, this._touchE);
                            const zoom = length / this._scaleRef;
                            this._scaleRef = length;
                            this.zoomDelta((1 - zoom) * this._ops.touchZoomRatio!);
                        }
                        if (this._ops.useTranslate) {
                            this._transTmp.copyFrom(this._touchS).addInPlace(this._touchE).scaleInPlace(0.5);
                            this.moveDelta(this._transTmp.x - this._transRef.x, this._transTmp.y - this._transRef.y);
                            this._transRef.copyFrom(this._transTmp);
                        }
                    }

                    if (this._singleStart) {
                        this._pointerPress.set(e.event.x, e.event.y);
                        switch (this._isLeft ^ this._isTop ^ useRotate ^ useTranslate) {
                            case LEFT ^ TOP ^ CAN_ROT ^ CANNOT_TRS:
                            case LEFT ^ BOTTOM ^ CAN_ROT ^ CANNOT_TRS:
                            case RIGHT ^ TOP ^ CAN_ROT ^ CANNOT_TRS:
                            case RIGHT ^ BOTTOM ^ CAN_ROT ^ CANNOT_TRS:
                            case RIGHT ^ TOP ^ CAN_ROT ^ CAN_TRS:
                            case RIGHT ^ BOTTOM ^ CAN_ROT ^ CAN_TRS:
                                this.pointerRotate(e);
                                break;

                            case LEFT ^ TOP ^ CAN_ROT ^ CAN_TRS:
                            case LEFT ^ BOTTOM ^ CAN_ROT ^ CAN_TRS:
                            case LEFT ^ TOP ^ CANNOT_ROT ^ CAN_TRS:
                            case LEFT ^ BOTTOM ^ CANNOT_ROT ^ CAN_TRS:
                            case RIGHT ^ TOP ^ CANNOT_ROT ^ CAN_TRS:
                            case RIGHT ^ BOTTOM ^ CANNOT_ROT ^ CAN_TRS:
                                this.pointerMove(e);
                                break;
                        }
                    }
                    break;
                case PointerEventTypes.POINTERUP:
                    if (this._multiStart) {
                        this._multiStart = false;
                        this.autoRotate(true);
                    }
                    if (this._singleStart) {
                        this._singleStart = false;
                        AnimationFrame.cancel(this._pressReq);
                        this.autoRotate(true);
                    }
                    this._pointers.length = 0;
                    break;
                case PointerEventTypes.POINTERWHEEL:
                    if (this._ops.useZoom) {
                        const we = evt.event as WheelEvent;
                        this.zoomDelta(we.deltaY * this._ops.wheelZoomRatio!);
                    }
                    break;
            }
        };
    }

    private setPointer(e: PointerEvent): void {
        for (let i = 0; i < this._pointers.length; i++) {
            if (this._pointers[i].pointerId == e.pointerId) {
                this._pointers[i] = e;
            }
        }
    }

    private hasPointer(e: PointerEvent): number {
        for (let i = 0; i < this._pointers.length; i++) {
            if (this._pointers[i].pointerId == e.pointerId) return i;
        }
        return -1;
    }

    private moveDelta(x: number, y: number): void {
        this._modelContainerP.position.x += x * this._ops.touchMoveRatio!;
        this._modelContainerP.position.y -= y * this._ops.touchMoveRatio!;
    }

    private zoomDelta(delta: number): void {
        if (AnimationFrame.isRunning(this._zoomWheelReq)) {
            this._zoomPrg = 0;
            this._zoomSpd = delta;
            return;
        }
        this.autoRotate(false);
        const speed = 0.05;
        AnimationFrame.request(() => {
            this._zoomPrg += speed;

            const s = this._modelContainerP.scaling.x - this._zoomSpd * (1 - this._zoomPrg);
            const scl = Math.max(Math.min(this._ops.maxZoom!, s), this._ops.minZoom!);
            this._modelContainerP.scaling.setAll(scl);
            if (this._zoomPrg >= 1) {
                this.autoRotate(true);
                return true;
            }

        }, 16, 200, this._zoomWheelReq);
    }

    private autoRotate(start: boolean): void {
        if (!this._ops.autoRotate) return;
        if (!start) {
            AnimationFrame.cancel(this._autoRotateReq);
            return;
        }
        AnimationFrame.throttleByTime(this._autoRotateDelayReq, this._ops.autoRotateDelay!, () => {
            AnimationFrame.request(() => {
                this._modelContainer.rotationQuaternion?.multiplyInPlace(this._ops.autoRotateDelta!);
            }, 16, 200, this._autoRotateReq);
        }, false);
    }

    private checkPress(e: PointerInfo, delta: number): void {
        let timer = 0;
        const sx = e.event.x;
        const sy = e.event.y;
        const thres = this._ops.pressArea!
        const pr = this.scene.getEngine().getHardwareScalingLevel();
        AnimationFrame.request((t) => {
            timer += t;
            if (timer > this._ops.pressThreshold!) {
                const dx = (this._pointerPress.x - sx) * pr;
                const dy = (this._pointerPress.y - sy) * pr;
                const dis = dx * dx + dy * dy;
                if (dis > thres) {
                    return true;
                } else {
                    this.pointerZoom(e, delta, t);
                }
            }

        }, 16, 200, this._pressReq);
    }

    private pointerZoom(e: PointerInfo, delta: number, interval: number): void {
        const spd = this._ops.speedRatio! * delta * interval * 0.0005;
        const scl = Math.max(Math.min(this._ops.maxZoom!, this._modelContainerP.scaling.x - spd), this._ops.minZoom!);
        this._modelContainerP.scaling.set(scl, scl, scl);
    }

    private pointerMove(e: PointerInfo): void {
        const scl = this._ops.speedRatio! * 0.0005;
        const dx = e.event.x - this._pointerMove.x;
        const dy = e.event.y - this._pointerMove.y;
        this._modelContainerP.position.x += dx * scl;
        this._modelContainerP.position.y -= dy * scl;
        this._pointerMove.set(e.event.x, e.event.y);
    }

    private pointerRotate(e: PointerInfo): void {
        const pi = e.pickInfo;
        if (!pi) return;
        const ray = pi.ray;
        if (!ray) return;
        const dir = ray.direction;
        if (this._rc > 0) {
            this._preDir.copyFrom(dir);
        }
        if (Math.abs(Vector3.Dot(dir, Vector3.UpReadOnly)) > this._ops.verticalRotateLimit!) return;
        const ha = Vector3.GetAngleBetweenVectorsOnPlane(this._preDir, dir, Vector3.UpReadOnly);
        const ax = this._tv0;
        this._container.getDirectionToRef(Vector3.LeftReadOnly, ax);
        const va = Vector3.GetAngleBetweenVectorsOnPlane(this._preDir, dir, ax);
        const rotH = Quaternion.RotationAxisToRef(Vector3.UpReadOnly, -ha * this._ops.speedRatio!, this._tq0);
        const rotV = Quaternion.RotationAxisToRef(Vector3.LeftReadOnly, -va * this._ops.speedRatio!, this._tq1);
        this._modelContainerV.rotationQuaternion!.multiplyInPlace(rotV);
        this._modelContainerH.rotationQuaternion!.multiplyInPlace(rotH);

        this._preDir.copyFrom(dir);
        this._rc = 0;
        AnimationFrame.throttleByTime(this._req, this._ops.rotateDelay!, () => {
            this._rc++;
        }, true);
    }

    /**
     * 是否启用
     */
    public get enabled(): boolean {
        return this._enabled;
    }

    /**
     * 启用观察层
     */
    public enable(): void {
        this._enabled = true;
        switch (this._ops.hideMode) {
            case 'visible':
                Helper.getChildMeshes(this._container).forEach(m => m.isVisible = true);
                break;
            case 'scaling':
                this._container.scaling.set(1, 1, 1);
                break;
        }

        this.detachEvent();
        this.attachEvent();
        this.autoRotate(true);
    }

    /**
     * 禁用观察层
     */
    public disable(): void {
        this.autoRotate(false);
        this._enabled = false;

        AnimationFrame.cancel(this._pressReq);

        switch (this._ops.hideMode) {
            case 'visible':
                Helper.getChildMeshes(this._container).forEach(m => m.isVisible = false);
                break;
            case 'scaling':
                this._container.scaling.set(0, 0, 0);
                break;
        }

        this.detachEvent();
    }

    /**
     * 更新渲染顺序
     */
    public update(): void {
        this._mask.renderingGroupId = this._ops.maskRenderingGroupId!;
        Helper.getChildMeshes(this._modelContainerV).forEach(m => {
            m.renderingGroupId = this._ops.modelRenderingGroupId!;
        });
    }

    /**
     * 重设容器旋转
     */
    public resetTransform(): void {
        this._modelContainerV.rotationQuaternion!.set(0, 0, 0, 1);
        this._modelContainerH.rotationQuaternion!.set(0, 0, 0, 1);
        this._modelContainer.rotationQuaternion!.set(0, 0, 0, 1);
        this._modelContainerP.position.set(0, 0, 0);
        this._modelContainerP.scaling.set(1, 1, 1);
    }

    /**
     * 获取模型容器
     */
    public get targetContainer(): TransformNode {
        return this._modelContainer;
    }

    /**
     * 获取组件容器
     */
    public get body(): TransformNode {
        return this._container;
    }

    /**
     * 获取遮罩面
     */
    public get mask(): AbstractMesh {
        return this._mask;
    }

    /**
     * 侦听触点事件
     */
    private attachEvent(): void {
        this.scene.onPointerObservable.add(this._pointerHandle.bind(this));
    }

    /**
     * 解除触点事件侦听
     */
    private detachEvent(): void {
        this.scene.onPointerObservable.removeCallback(this._pointerHandle.bind(this));
    }

    /**
     * 初始化遮罩平面
     */
    private initializeMask(): void {
        const pn = MeshBuilder.CreatePlane('observe-layer-mask', { size: this._ops.maskSize! }, this.scene);
        const mat = new StandardMaterial('observe-layer-mask-material', this.scene);
        mat.emissiveColor = this._ops.maskColor!;
        mat.disableLighting = true;
        mat.cullBackFaces = false;
        pn.material = mat;
        pn.visibility = this._ops.maskOpacity!;
        pn.parent = this._container;
        pn.rotation.set(0, 0, 0);
        this._mask = pn;
    }

    /**
     * 初始化模型容器
     */
    private initializeModelContainer(): void {
        this._modelContainerP = new TransformNode('observe-layer-model-container-p', this.scene);
        this._modelContainerP.parent = this._container;
        this._modelContainerP.position.set(0, 0, 0);
        this._modelContainerP.scaling.set(1, 1, 1);

        this._modelContainerV = new TransformNode('observe-layer-model-container-v', this.scene);
        this._modelContainerV.parent = this._modelContainerP;
        this._modelContainerV.position.set(0, 0, 0);
        this._modelContainerV.rotationQuaternion = Quaternion.FromEulerAngles(0, 0, 0);

        this._modelContainerH = new TransformNode('observe-layer-model-container-h', this.scene);
        this._modelContainerH.parent = this._modelContainerV;
        this._modelContainerH.position.set(0, 0, 0);
        this._modelContainerH.rotationQuaternion = Quaternion.FromEulerAngles(0, 0, 0);

        this._modelContainer = new TransformNode('observe-layer-model-container', this.scene);
        this._modelContainer.parent = this._modelContainerH;
        this._modelContainer.position.set(0, 0, 0);
        this._modelContainer.rotationQuaternion = Quaternion.FromEulerAngles(0, 0, 0);
    }

}


export { ObserveLayer };
export type { IObserveLayerOptions };